package com.gitee.zycra.file.pool;

import com.gitee.zycra.file.config.ConnectionConfig;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
import org.apache.commons.pool2.DestroyMode;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Factory for create SFTP connection session by connection config and maintains by keyed pool.
 *
 * @author zycra
 * @since 1.0.0
 */
public class SessionFactory extends BaseKeyedPooledObjectFactory<ConnectionConfig, ChannelSftp> {

    private static final Logger LOGGER = LoggerFactory.getLogger(SessionFactory.class);

    /**
     * Timeout for create the session.
     *
     * @since 1.0.0
     */
    private static final int SESSION_TIMEOUT = 60000;

    /**
     * The session map, key is connection config object and value is the created session.
     *
     * @since 1.0.0
     */
    private static final Map<ConnectionConfig, Session> SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * Create {@link ChannelSftp} instance by connection config.
     *
     * @param connectionConfig the connection config.
     * @return {@link ChannelSftp} instance.
     * @throws Exception create exception.
     * @since 1.0.0
     */
    @Override
    public ChannelSftp create(ConnectionConfig connectionConfig) throws Exception {
        Session session = getSession(connectionConfig);
        if (session == null) {
            throw new JSchException("session create failed");
        }
        Channel channel = session.openChannel("sftp");
        channel.connect();
        ChannelSftp channelSftp = (ChannelSftp) channel;
        LOGGER.info("SFTP connect success host={}, userName={}", connectionConfig.getHost(), connectionConfig.getUsername());
        return channelSftp;
    }

    /**
     * Wrap the created {@link ChannelSftp} instance to pooled object.
     *
     * @param channelSftp {@link ChannelSftp} instance.
     * @return wrapped pooled object.
     * @since 1.0.0
     */
    @Override
    public PooledObject<ChannelSftp> wrap(ChannelSftp channelSftp) {
        return new DefaultPooledObject<>(channelSftp);
    }

    /**
     * Check the object valid or not.
     *
     * @param key the connection config instance.
     * @param p   the pooled object instance.
     * @return true if the object is valid, false otherwise.
     * @since 1.0.0
     */
    @Override
    public boolean validateObject(ConnectionConfig key, PooledObject<ChannelSftp> p) {
        return p.getObject().isConnected();
    }

    /**
     * Destroy the pooled object.
     *
     * @param key         the connection config instance.
     * @param p           the pooled object instance.
     * @param destroyMode destroy mode.
     * @throws Exception destroy exception.
     * @since 1.0.0
     */
    @Override
    public void destroyObject(ConnectionConfig key, PooledObject<ChannelSftp> p, DestroyMode destroyMode) throws Exception {
        p.getObject().disconnect();
    }

    /**
     * Get the session by connection config from {@link #SESSION_MAP}, when get failed, create a new session and put it in the pool.
     *
     * @param connectionConfig connection config.
     * @return session instance of the connection config.
     * @since 1.0.0
     */
    private synchronized Session getSession(ConnectionConfig connectionConfig) {
        Session session = SESSION_MAP.get(connectionConfig);
        if (session == null) {
            session = initSession(connectionConfig);
            if (session == null) {
                return null;
            }
            SESSION_MAP.put(connectionConfig, session);
        }
        if (session.isConnected()) {
            return session;
        }
        try {
            session.disconnect();
            session.connect();
            return session;
        } catch (Exception e) {
            LOGGER.error("session connect exception", e);
        }
        return null;
    }

    /**
     * Init session with given configuration.
     *
     * @param connectionConfig the connection config.
     * @return session instance of the connection config.
     * @since 1.0.0
     */
    private Session initSession(ConnectionConfig connectionConfig) {
        try {
            JSch jsch = new JSch();
            String connectKey = connectionConfig.getConnectionKey();
            if (connectKey != null && !connectKey.isBlank()) {
                String passphrase = connectionConfig.getPassphrase();
                jsch.addIdentity(null, connectKey.getBytes(StandardCharsets.UTF_8), null,
                        passphrase == null || passphrase.isBlank() ? null : passphrase.getBytes(StandardCharsets.UTF_8));
            }
            String host = connectionConfig.getHost();
            String userName = connectionConfig.getUsername();
            LOGGER.info("SFTP connect host={}, userName={}", host, userName);
            Session session = jsch.getSession(userName, host, connectionConfig.getPort());
            LOGGER.info("SFTP session created host={}, userName={}", host, userName);
            session.setPassword(connectionConfig.getPassword());
            session.setConfig("StrictHostKeyChecking", "no");
            session.setConfig("kex", "diffie-hellman-group1-sha1");
            session.setTimeout(SESSION_TIMEOUT);
            session.connect();
            LOGGER.info("SFTP session connect success host={}, userName={}", host, userName);
            return session;
        } catch (Exception e) {
            LOGGER.error("session create exception", e);
        }
        return null;
    }
}
